from __future__ import division

import math
import random

from .attributes import Attribute, Object
from .memoized import memoized


class Position(object):
    def __init__(self, x, y = None):
        if y is None:
            self._x = x[0]
            self._y = x[1]
        else:
            self._x = x
            self._y = y
    
    @property
    def inheritable_attributes(self):
        return {}
    
    @property
    def x(self):
        return self._x
    
    @property
    def y(self):
        return self._y
    
    @property
    def position(self):
        return self
    
    @property
    def rounded(self):
        return Position(int(self._x + 0.5),
                        int(self._y + 0.5))
    
    @property
    def integral(self):
        return Position(int(self._x), int(self._y))

    @property
    def tuple(self):
        return (self._x, self._y)
    
    def tuple_in_dimension(self, n):
        assert n >= 2
        
        return tuple([self._x, self._y] + [0] * (n - 2))
    
    @property
    def neighbours(self):
        for direction in DIRECTIONS | DIAGONALS:
            yield self.in_direction(direction)
    
    def in_direction(self, dx, dy = None):
        if dy is None:
            dx, dy = dx.tuple
        
        return self.__class__(self._x + dx, self._y + dy, **self.inheritable_attributes)
    
    def direction_of(self, position):
        dx = dy = 0
        
        if self._x < position.x:
            dx = +1
        
        if self._x > position.x:
            dx = -1
        
        if self._y < position.y:
            dy = +1
        
        if self._y > position.y:
            dy = -1
        
        return Direction(dx, dy)
    
    def towards(self, position):
        return self.in_direction(self.direction_of(position))
    
    def distance(self, position):
        return math.sqrt((self._x - position.x) ** 2 +
                         (self._y - position.y) ** 2)
            
    def radial_distance(self, position, radius, displacement = 0):
        delta = self.distance(position)
        ratio = (radius - delta) / radius + displacement
        
        return min(1.0, max(0.0, ratio))
    
    def random_nearby(self, radius):
        dx = random.randint(-radius, radius)
        dy = random.randint(-radius, radius)
        
        return self.in_direction(Direction(dx, dy))
    
    def extend_to(self, position, continuous = False):
        x1 = min(position.x, self.x)
        x2 = max(position.x, self.x)
        y1 = min(position.y, self.y)
        y2 = max(position.y, self.y)
        
        if continuous:
            return Position(x1, y1).extend_by(x2 - x1,     y2 - y1,     continuous)
        else:
            return Position(x1, y1).extend_by(x2 - x1 + 1, y2 - y1 + 1, continuous)
    
    def extend_by(self, width, height, continuous = False):
        return Area(self, width, height, continuous)
    
    @property
    def area(self):
        return self.extend_by(1, 1, True)
    
    def __iter__(self):
        return iter(self.tuple)
    
    def __sub__(self, other):
        return self.vector - other.vector
    
    def __eq__(self, position):
        if position is None:
            return False
        else:
            return self._x == position.x and self._y == position.y
    
    def __hash__(self):
        return self._x * 1000000  + self._y * 1000
    
    def __repr__(self):
        return 'Position(%r, %r)' % self.tuple
   
    def __str__(self):
        return '(%s, %s)' % self.tuple


class Direction(Object):
    dx = Attribute()
    dy = Attribute()
    
    @property
    def tuple(self):
        return (self.dx, self.dy)
    
    @property
    def reverse(self):
        return Direction(-self.dx, -self.dy)
    
    @property
    def length(self):
        return math.sqrt(self.dx ** 2 + self.dy ** 2)
    
    def with_length(self, length):
        return Direction(self.dx * length / self.length,
                         self.dy * length / self.length)
    
    def __repr__(self):
        return 'Direction(%r, %r)' % self.tuple
    
    def __str__(self):
        return 'd(%s, %s)' % self.tuple
    
    def __eq__(self, direction):
        return self.dx == direction.dx and self.dy == direction.dy
    
    def __hash__(self):
        return hash(self.tuple)


DIRECTIONS = set([Direction(x, y) for x, y in
                  [(0, -1), (1, 0), ( 0, 1), (-1,  0)]])
DIAGONALS  = set([Direction(x, y) for x, y in
                  [(1, 1), (1, -1), (-1, 1), (-1,  -1)]])


class Area(Object):
    top_left      = Attribute()
    width         = Attribute()
    height        = Attribute()
    is_continuous = Attribute(initial = False)
    top           = top_left._.y
    left          = top_left._.x
    
    def setup(self):
        if self.width < 0:
            self._top_left = Position(self.left + self.width, self.top)
            self._width    = -self.width
        
        if self.height < 0:
            self._top_left = Position(self.left, self.top + self.height)
            self._height   = -self.height
    
    @property
    def inheritable_attributes(self):
        return {}
    
    @property
    def size(self):
        return (self.width, self.height)
    
    @property
    @memoized
    def diagonal(self):
        return Direction(*self.size)
    
    @property
    @memoized
    def bottom_right(self):
        if self.is_continuous:
            return self.top_left.in_direction(self.diagonal)
        else:
            return self.top_left.in_direction(self.diagonal).in_direction(-1, -1)
    
    @property
    def bottom(self):
        return self.bottom_right.y
    
    @property
    def right(self):
        return self.bottom_right.x
    
    @property
    @memoized
    def top_right(self):
        return Position(self.right, self.top)
    
    @property
    @memoized
    def bottom_left(self):
        return Position(self.left, self.bottom)
    
    def includes(self, position):
        return (self.left <= position.x <= self.right and
                self.top  <= position.y <= self.bottom)
    
    def grown_by(self, x, y = None):
        if y is None:
            y = x
        
        return self.__class__(self.top_left.in_direction(-x, -y),
                              self.width + 2 * x, self.height + 2 * y,
                              **self.inheritable_attributes)
    
    def in_direction(self, x, y):
        return self.__class__(self.top_left.in_direction(x, y),
                              self.width, self.height,
                              **self.inheritable_attributes)
    
    def with_width(self, width):
        return self.__class__(self.top_left, width, self.height,
                              **self.inheritable_attributes)
    
    def with_height(self, height):
        return self.__class__(self.top_left, self.width, height,
                              **self.inheritable_attributes)
    
    def sized(self, width, height = None):
        if height is None:
            height = width
        
        return self.__class__(self.top_left, width, height,
                              **self.inheritable_attributes)
    
    def moved_right(self, n):
        return self.in_direction(n, 0)
    
    def moved_left(self, n):
        return self.moved_right(-n)
    
    def moved_down(self, n):
        return self.in_direction(0, n)
    
    def moved_up(self, n):
        return self.moved_down(-n)
    
    @property
    def adjacent(self):
        for position in self.grown_by(1):
            if not self.includes(position):
                yield position
    
    def adjacent_with(self, predicate):
        for position in self.adjacent:
            if predicate(position):
                yield position
    
    @property
    def positions(self):
        for dy in range(self.height):
            for dx in range(self.width):
                yield self.top_left.in_direction(dx, dy)
    
    @property
    def random_position(self):
        return random.choice(list(self.positions))
    
    def __contains__(self, position):
        return self.includes(position)
    
    def __iter__(self):
        assert not self.is_continuous
        
        return iter(self.positions)
    
    def __repr__(self):
        return '<%s-%s>' % (self.top_left, self.bottom_right)
